#!/usr/bin/env bash

[[ -z "${SOURCED+set}" ]] && declare -A SOURCED
SOURCED+=('inc/common.sh')

# To store environment related information
declare -A ENV_INFO

if [ -n "$(command -v doas)" ]; then
	subin='doas'
else
	subin='sudo'
fi

# if realpath is not there on system
if [ -z "$(command -v realpath)" ]; then
	function realpath(){
		if [ -d "$1" ]; then
			cd "$1"
			pwd
		elif [ -f "$1" ]; then
			cd "$(dirname $1)"
			echo "$(pwd)/$(basename $1)"
		fi
	}
fi

# Store repo root path
REPO_ROOT="$(dirname $(realpath "$(dirname $0)"))"

# Config variations available
declare -A CONFIG_VARIATIONS=(
		[1]='default'
		[2]='dark'
	)

# Type of message
declare -A MESSAGE_TYPES=(
		[none]=0
		[action]=10
		[info]=20
		[warning]=30
		[error]=60
		[fatal]=90
	)

# Terminal text color
declare -A TCOLOR=(
		[red]='\033[1;31m'
		[green]='\033[1;32m'
		[yellow]='\033[1;33m'
		[blue]='\033[1;34m'
		[purple]='\033[1;35m'
		[cyan]='\033[1;36m'
		[none]='\033[0m'
	)

# Basic stuff
TRUE=1
FALSE=0

# Print a message on screen.
# Params:
#   - $1: The message to show
#   - $2: The type of message (see 'MESSAGE_TYPES' declaration for details) (optional)
#   - $3: Any options to pass to echo. Such as '-n'. (optional)
function show_message() {
	if [ "$2" = "$MESSAGE_TYPES[info]" ]; then
		echo $3 -e " -> ${1}"
	elif [ "$2" = "$MESSAGE_TYPES[action]" ]; then
		echo $3 -e "${TCOLOR[green]} >> ${1}${TCOLOR[none]}"
	elif [ "$2" = "$MESSAGE_TYPES[warning]" ]; then
		echo $3 -e "${TCOLOR[yellow]} !! ${1}${TCOLOR[none]}"
	elif [ "$2" = "$MESSAGE_TYPES[error]" ]; then
		echo $3 -e "${TCOLOR[blue]} ** ${1}${TCOLOR[none]}"
	elif [ "$2" = "$MESSAGE_TYPES[fatal]" ]; then
		echo $3 -e "${TCOLOR[red]} @@ ${1}${TCOLOR[none]}"
	else
		echo $3 -e " ## ${1}"
	fi
}

function get_operating_system_kind() {
	echo $(uname -s | tr '[:upper:]' '[:lower:]')
}
ENV_INFO+=(["operating_system_kind"]="$(get_operating_system_kind)")

function get_operating_system_name() {
	if [ -f /etc/os-release ]; then
		name=$(sed -ne 's/^DISTRIB_ID="\([^"]*\)"$/\1/p' /etc/os-release)
		[ -z "$name" ] && name=$(sed -ne 's/^ID=\(.*\)/\1/p' /etc/os-release)
		echo "$name"
	else
		uname -s | tr '[:upper:]' '[:lower:]'
	fi
}
ENV_INFO+=(["operating_system_name"]="$(get_operating_system_name)")

function get_package_manager() {
	if [ ! -z "$(command -v apt)" ]; then # Debian/Trisquel
		echo 'apt'
	elif [ ! -z "$(command -v pacman)" ]; then # Arch/Parabola
		echo 'pacman'
	elif [ ! -z "$(command -v pkg)" ]; then # FreeBSD
		echo 'pkg'
	elif [ ! -z "$(command -v pkg_add)" ]; then # OpenBSD etc.
		echo 'pkg_*'
	elif [ ! -z "$(command -v xbps-install)" ]; then # Void/Ymir
		echo 'xbps'
	elif [ ! -z "$(command -v apk)" ]; then # Apline
		echo 'apk'
	fi
}
ENV_INFO+=(["package_manager"]="$(get_package_manager)")

# Store package search cache filepath
ENV_INFO+=(["installer_cache_dir"]="$HOME/.cache/openbox-config/installer")
ENV_INFO+=(["search_cache_available"]="${ENV_INFO[installer_cache_dir]}/package_search_available")
ENV_INFO+=(["search_cache_installed"]="${ENV_INFO[installer_cache_dir]}/package_search_installed")

# Store all the available packages (installed+uninstalled) for search cache
function package_names_cache() {
	# If already cached, don't bother
	[ -f "${ENV_INFO[search_cache_available]}" ] && skip_available=1
	[ -f "${ENV_INFO[search_cache_installed]}" ] && skip_installed=1

	mkdir -p "${ENV_INFO[installer_cache_dir]}"

	# Write package names in cache files.
	# No version info is needed to be saved. They're intended to only contain package names, each on its own line.
	if [ "${ENV_INFO['package_manager']}" = 'apt' ]; then
		[ "$skip_available" != "1" ] && grep ^Package /var/lib/apt/lists/*_Packages | awk '{print $2}' > "${ENV_INFO[search_cache_available]}"
		[ "$skip_installed" != "1" ] && apt list --installed | awk -F'/' '{print $1}' > "${ENV_INFO[search_cache_installed]}"
	elif [ "${ENV_INFO['package_manager']}" = 'pacman' ]; then
		[ "$skip_available" != "1" ] && pacman -Sl | awk '{print $2}' > "${ENV_INFO[search_cache_available]}"
		[ "$skip_installed" != "1" ] && pacman -Q > "${ENV_INFO[search_cache_installed]}"
	elif [ "${ENV_INFO['package_manager']}" = 'pkg' ]; then
		[ "$skip_available" != "1" ] && pkg search '.*' | sed -ne 's|\(.*\)-[0-9].*$|\1|p' > "${ENV_INFO[search_cache_available]}"
		[ "$skip_installed" != "1" ] && pkg info -a > "${ENV_INFO[search_cache_installed]}"
	elif [ "${ENV_INFO['package_manager']}" = 'pkg_*' ]; then
		[ "$skip_available" != "1" ] && curl https://cdn.openbsd.org/pub/OpenBSD/`uname -r`/packages/`uname -m`/ | grep '\.tgz</a>' | sed -n 's/.*">\(.*\)-[0-9].*\.tgz.*/\1/p' > "${ENV_INFO[search_cache_available]}"
		[ "$skip_installed" != "1" ] && pkg_info -a | sed -ne 's|\(.*\)-[0-9].*$|\1|p' > "${ENV_INFO[search_cache_installed]}"
	elif [ "${ENV_INFO['package_manager']}" = 'xbps' ]; then
		[ "$skip_available" != "1" ] && xbps-query -Rs '*' | awk '{print $2}' | sed -ne 's|^\(.*\)\-.*[0-9]_[0-9]|\1|p' > "${ENV_INFO[search_cache_available]}"
		[ "$skip_installed" != "1" ] && xbps-query -Rl | awk '{print $2}' | sed -ne 's|^\(.*\)\-.*[0-9]_[0-9]|\1|p' > "${ENV_INFO[search_cache_installed]}"
	elif [ "${ENV_INFO['package_manager']}" = 'apk' ]; then
		[ "$skip_available" != "1" ] && apk search | sed -ne 's|\(.*\)-.*-r[0-9]*|\1|p' > "${ENV_INFO[search_cache_available]}"
		[ "$skip_installed" != "1" ] && apk info > "${ENV_INFO[search_cache_installed]}"
	else
		show_message "Error: The package manager ${ENV_INFO['package_manager']} is not supported" $MESSAGE_TYPES[error]; exit 67324
	fi
}

# Searches for a package
# Params:
#   - $1: Package name to search
# Return: Prints the search results
function package_search() {
	package_names_cache
	if [ -f "${ENV_INFO[search_cache_available]}" ]; then
		grep "$1" "${ENV_INFO[search_cache_available]}"
	else
		if [ "${ENV_INFO['package_manager']}" = 'apt' ]; then
			dpkg -l $1
		elif [ "${ENV_INFO['package_manager']}" = 'pacman' ]; then
			pacman -Q $1
		elif [ "${ENV_INFO['package_manager']}" = 'pkg' ]; then
			pkg search $1
		elif [ "${ENV_INFO['package_manager']}" = 'pkg_*' ]; then
			pkg_search
		elif [ "${ENV_INFO['package_manager']}" = 'xbps' ]; then
			xbps-query -Rs $1
		elif [ "${ENV_INFO['package_manager']}" = 'apk' ]; then
			apk search $1
		else
			show_message "Error: The package manager ${ENV_INFO['package_manager']} is not supported" $MESSAGE_TYPES[error]; exit 34648
		fi
	fi
}

# Checks if a package is availablein repos
# Params:
#   - $1: Package name to search. (No regex patterns.)
#   - $2: Regex pattern in package name. e.g. 'py[0-9][0-9]-pip' (optional)
function is_package_available() {
	package_names_cache

	if [ -z "$2" ]; then
		local pattern="$1"
	else
		local pattern="$2"
	fi

	if [ -f "${ENV_INFO[search_cache_available]}" ]; then
		grep "$1" "${ENV_INFO[search_cache_available]}" | grep "^${pattern}$" &>/dev/null
	else
		search_result="$(package_search $1)"

		if [ "${ENV_INFO['package_manager']}" = 'apt' ]; then
			echo "$search_result" | grep "^${pattern}-[0-9]" &>/dev/null
		elif [ "${ENV_INFO['package_manager']}" = 'pacman' ]; then
			echo "$search_result" | grep "^${pattern}-[0-9]" &>/dev/null
		elif [ "${ENV_INFO['package_manager']}" = 'pkg' ]; then
			echo "$search_result" | grep "^${pattern}-[0-9]" &>/dev/null
		elif [ "${ENV_INFO['package_manager']}" = 'pkg_*' ]; then
			echo "$search_result" | grep "^${pattern}-[0-9]" &>/dev/null
		elif [ "${ENV_INFO['package_manager']}" = 'xbps' ]; then
			echo "$search_result" | grep "\s${pattern}-[0-9]" &>/dev/null
		elif [ "${ENV_INFO['package_manager']}" = 'apk' ]; then
			echo "$search_result" | grep "^${pattern}-[0-9]" &>/dev/null
		else
			show_message "Error: The package manager ${ENV_INFO['package_manager']} is not supported" $MESSAGE_TYPES[error]; exit 34648
		fi
	fi

	# Process the return code from search+grep
	if [ "$?" -eq "0" ]; then
		echo $TRUE
	else
		echo $FALSE
	fi
}

# Checks if a package is installed in repos
# Params:
#   - $1: Package name
function is_package_installed() {
	package_names_cache

	if [ -f "${ENV_INFO[search_cache_installed]}" ]; then
		grep "^${1}$" "${ENV_INFO[search_cache_installed]}"
	else
		if [ "${ENV_INFO['package_manager']}" = 'apt' ]; then
			dpkg -l $1 &>/dev/null
		elif [ "${ENV_INFO['package_manager']}" = 'pacman' ]; then
			pacman -Q $1 &>/dev/null
		elif [ "${ENV_INFO['package_manager']}" = 'pkg' ]; then
			pkg info $1 &>/dev/null
		elif [ "${ENV_INFO['package_manager']}" = 'pkg_*' ]; then
			pkg_info | grep "^${1}--[0-9]" &>/dev/null
		elif [ "${ENV_INFO['package_manager']}" = 'xbps' ]; then
			xbps-query -S $1 &>/dev/null
		elif [ "${ENV_INFO['package_manager']}" = 'apk' ]; then
			apk info | grep "^${1}$" &>/dev/null
		else
			show_message "Error: The package manager ${ENV_INFO['package_manager']} is not supported" $MESSAGE_TYPES[error]; exit 34648
		fi
	fi

	# Process the return code from last command
	if [ "$?" -eq "0" ]; then
		echo $TRUE
	else
		echo $FALSE
	fi
}

# Trim leading and trailing spaces
# Params:
#   - $1: Input string to strip
function trim_spaces() {
	echo "$1" | awk '{$1=$1;print}'
}

# Returns random alphanumeric 32 chars
function random_string() {
	tr -dc '[:alnum:]' < /dev/urandom | dd bs=4 count=8 2>/dev/null
}

# Removes duplicates from space separated string.
# As a side effect, whitespaces are trimmed from beginning and end.
# Params:
#   - $1: The string to process
function remove_duplicates() {
	echo "$1" | tr ' ' '\n' | sort -u | tr '\n' ' ' | sed -e 's/[[:space:]]*$//'
}

# Installs packages
# Params:
#   - $1: Packages to install (space separated)
function install_packages() {
	packages="$1"
	for package in $packages; do
		if [ ! -z "$package" ]; then
			if [ "$(is_package_available $package)" = "$FALSE" ]; then
				show_message "$package is not found on repo!!" $MESSAGE_TYPES[warning]
				packages=$(echo "$packages " | sed -e "s|[[:space:]]$package[[:space:]]| |")
				nonexisting_packages+=" $package"
			fi
		fi
	done
	# Trim the variables to look and work nice
	packages=$(trim_spaces "$packages")
	nonexisting_packages=$(trim_spaces "$nonexisting_packages")
	if [ ! -z "$nonexisting_packages" ]; then
		show_message "These packages not found on repository: $nonexisting_packages" $MESSAGE_TYPES[warning]
		show_message "Please install them through other means (such as PPAs, AUR or just build from source)." $MESSAGE_TYPES[warning]
	fi
	if [ ! -z "$packages" ]; then
		show_message "Packages going to be installed: $packages" $MESSAGE_TYPES[info]

		if [ "${ENV_INFO['package_manager']}" = 'apt' ]; then
			$subin apt install $packages
		elif [ "${ENV_INFO['package_manager']}" = 'pacman' ]; then
			$subin pacman -S $packages
		elif [ "${ENV_INFO['package_manager']}" = 'pkg' ]; then
			$subin pkg install $packages
		elif [ "${ENV_INFO['package_manager']}" = 'pkg_*' ]; then
			$subin pkg_add $packages
		elif [ "${ENV_INFO['package_manager']}" = 'xbps' ]; then
			$subin xbps-install $packages
		elif [ "${ENV_INFO['package_manager']}" = 'apk' ]; then
			$subin apk add $packages
		else
			echo "Error: The package manager ${ENV_INFO['package_manager']} is not supported"
		fi
	fi
}

# Removes packages
# Params:
#   - $1: Packages to remove (space separated)
function remove_packages() {
	packages="$1"
	for package in $packages; do
		if [ ! -z "$package" ]; then
			if [ "$(is_package_available $package)" = "$FALSE" ]; then
				show_message "$package is not found on repo!!" $MESSAGE_TYPES[warning]
				packages=$(echo "$packages " | sed -e "s|[[:space:]]$package[[:space:]]| |")
				nonexisting_packages+=" $package"
			fi
		fi
	done
	# Trim the variables to look and work nice
	packages=$(trim_spaces "$packages")
	nonexisting_packages=$(trim_spaces "$nonexisting_packages")
	if [ ! -z "$nonexisting_packages" ]; then
		show_message "These packages not found on repository: $nonexisting_packages" $MESSAGE_TYPES[warning]
		show_message "So can't remove package." $MESSAGE_TYPES[warning]
	fi
	if [ ! -z "$packages" ]; then
		show_message "Packages going to be removed: $packages" $MESSAGE_TYPES[info]

		if [ "${ENV_INFO['package_manager']}" = 'apt' ]; then
			$subin apt autoremove $packages
		elif [ "${ENV_INFO['package_manager']}" = 'pacman' ]; then
			$subin pacman -Rs $packages
		elif [ "${ENV_INFO['package_manager']}" = 'pkg' ]; then
			$subin pkg autoremove $packages
		elif [ "${ENV_INFO['package_manager']}" = 'pkg_*' ]; then
			$subin pkg_delete $packages
		elif [ "${ENV_INFO['package_manager']}" = 'xbps' ]; then
			$subin xbps-remove $packages
		elif [ "${ENV_INFO['package_manager']}" = 'apk' ]; then
			$subin apk del $packages
		else
			echo "Error: The package manager ${ENV_INFO['package_manager']} is not supported"
		fi
	fi
}

# Converts package name(s) according to current distro/system
# Params:
#   - $1: Package list (space separated)
#   - $2: Ignore already installed packages. $TRUE or $FALSE. (optional)
# Returns: string with valid package names
function convert_package_names() (
	list=''
	# Params:
	#   - $1: Package name
	#   - $2: Forwarded $2
	function _add_to_list() {
		if [ "$2" = "$TRUE" ]; then
			if [ "$(is_package_installed "${1}")" = "$FALSE" ]; then
				echo -n " ${1}"
			fi
		else
			echo -n " ${1}"
		fi
	}

	# Remove duplicates to reduce time needed
	packages=$(remove_duplicates "$1")

	for package in $packages; do
		if [ ! -z "$package" ]; then
			# FreeBSD exceptions for package names
			if [ "${ENV_INFO[operating_system_name]}" = 'freebsd' ] || [ "${ENV_INFO[operating_system_name]}" = 'openbsd' ]; then
				if [ "$package" = 'make' ]; then
					package='gmake'
				fi
			fi
			# If package is available, then simply add it to return string
			if [ "$(is_package_available "${package}")" = "$TRUE" ]; then
				list+=$(_add_to_list "$package" "$2")
			# If package is not available, check for equivalent
			else
				# In a musl system, this/these package(s) should be found and installed automatically by previous if.
				# So we can safely ignore it.
				if [ "$package" = 'musl-dev' ]; then
					continue # ignore and go to next loop
				fi
				if [ "${ENV_INFO[operating_system_name]}" = 'void' ]; then
					# Exception for libX11's X being lowercase
					package=$(echo "$package" | sed -e 's/libx11/libX11/')
					# Find out package name without -dev or -devel
					package_basename=$(echo "$package" | sed -ne 's/\(.*\)-\(devel\|dev\)/\1/p')
					# Exception for gtk+ package
					if echo $package_basename | grep gtk\+ &>/dev/null; then
						package_basename=$(echo "$package" | sed -ne 's|\(gtk+[0-9]\).*|\1|p')
					fi
					if [ "$(is_package_available ${package_basename}-devel)" = "$TRUE" ]; then
						list+=$(_add_to_list "${package_basename}-devel" "$2")
					fi
				fi
			fi
		fi
	done
	echo -n "$list"
)

# Function that parses YAML data.
# Params:
#   - $1: YAML filename to parse
#   - $2: Prefix for variable names (optional)
# Usage:
#   eval $(parse_yaml sample.yml)
#   eval $(parse_yaml sample.yml 'SOMEPREFIX_')
# Written by Stefan Farestam
# Source: https://stackoverflow.com/a/21189044
function parse_yaml {
   local prefix=$2
   local s='[[:space:]]*' w='[a-zA-Z0-9_]*' fs=$(echo @|tr @ '\034')
   sed -ne "s|^\($s\):|\1|" \
        -e "s|^\($s\)\($w\)$s:$s[\"']\(.*\)[\"']$s\$|\1$fs\2$fs\3|p" \
        -e "s|^\($s\)\($w\)$s:$s\(.*\)$s\$|\1$fs\2$fs\3|p"  $1 |
   awk -F$fs '{
      indent = length($1)/2;
      vname[indent] = $2;
      for (i in vname) {if (i > indent) {delete vname[i]}}
      if (length($3) > 0) {
         vn=""; for (i=0; i<indent; i++) {vn=(vn)(vname[i])("_")}
         printf("%s%s%s=\"%s\"\n", "'$prefix'",vn, $2, $3);
      }
   }'
}

# Determines if a binary is installed or not
# Params:
#   - $1: Binary name
function is_binary_installed() {
	if [ -z "$(command -v $1)" ]; then
		echo $FALSE
	else
		echo $TRUE
	fi
}

# Determine if process is running.
# Params:
#   - $1: Process name
function is_process_running() {
	ps aux | grep "\s${1}$\|\/${1}\(\s\|$\)" &>/dev/null
	if [ "$?" -eq "0" ]; then
		echo $TRUE
	else
		echo $FALSE
	fi
}

# Get process ID for a process name.
# Params:
#   - $1: Process name
function get_process_id() {
	ps aux | grep "\s${1}$" | awk '{ print $2 }'
}

# Gets yaml data and loads YAML data into variables $yaml_abc_xyz
function get_yaml_data() {

	if [ -f "$REPO_ROOT/custom.default.yaml" ]; then
		eval $(parse_yaml "$REPO_ROOT/custom.default.yaml" 'yaml_')
	fi

	if [ -f "$HOME/.local/share/openbox-config/custom.yaml" ]; then
		eval $(parse_yaml "$HOME/.local/share/openbox-config/custom.yaml" 'yaml_')
	fi

	if [ -f "$REPO_ROOT/custom.yaml" ]; then
		eval $(parse_yaml "$REPO_ROOT/custom.yaml" 'yaml_')
	fi

	ENV_INFO+=(["parsed_yaml"]=true)

}
get_yaml_data

# List of programs required to run the config.
# These cannot be changed from custom.yaml or config files. So installing
# through the installer means these will always be installed. SLiM is not
# included because it has a separate option and using it is optional.
ENV_INFO+=(["required_packages"]="openbox
tint2
jgmenu
obmenu-generator
xdotool
wmctrl
rofi
papirus-icon-theme
volumeicon
cbatticon
")

# YAML variables with program names in them
ENV_INFO+=(["program_yaml_vars"]="
yaml_variables_file_manager_exec
yaml_variables_text_editor_exec
yaml_variables_text_editor_2_exec
yaml_variables_terminal_exec
yaml_variables_lock_command
yaml_variables_webbrowser_exec
yaml_variables_webbrowser_2_exec
yaml_variables_calendar_1_exec
yaml_variables_calendar_2_exec
")

# Goes through each program that is set in YAML config and finds out missing
# ones that need to be installed.
function get_missing_programs() {
	# If yaml data has not been parsed yet
	if [ ! "${ENV_INFO['parsed_yaml']}" ]; then
		get_yaml_data
	fi

	echo "${ENV_INFO['required_packages']}" | while IFS= read -r line; do
		if [ ! -z "$line" ]; then
			if [ "$(is_package_installed "${line}")" = "$FALSE" ]; then
				echo -n " ${line}"
			fi
		fi
	done

	echo "${ENV_INFO['program_yaml_vars']}" | while IFS= read -r line; do
		if [ ! -z "$line" ]; then
			if [ "$(is_binary_installed "${!line}")" = "$FALSE" ]; then
				echo -n " ${!line}"
			fi
		fi
	done
}
